/*
 * The MIT License
 *
 * Copyright (c) 2009 André D. Piske
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */
package org.gohtml;

import java.io.Reader;

/**
 * The HTML parser class.<br>
 * This will convert a HTML text to elements.
 *
 * <p>This should be able to parse very huge documents without consuming all
 * system memory. It will not cache elements, but instead it'll return each
 * element in a top-to-bottom parsing fashion.<br>
 * The result is like when iterating over a List.</p>
 *
 * <p>The usage should be simple as this:<br>
 * <code>Reader myReader = ...;<br>
 * Parser myParser = new Parser(myReader);<br>
 * while (myParser.hasNext()) {<br>
 *      Element e = myParser.next();<br>
 *      if (e != null) {<br>
 *          // use <b>e</b> here<br>
 *      }<br>
 * }<br>
 *</code>
 *
 * @author André D. Piske
 * @version 0.1 alpha
 * @since 0.1 alpha
 * @see org.gohtml.Element
 * @see java.io.Reader
 */
public final class Parser {

    private Reader docReader;
    private Backreader backreader;
    private String lastStartedNode;
    private boolean isEOF;
    private char inCache[];
    private int inCachePos, inCacheSize;
    protected final int STRING_BUFFER_SIZE = 1024;
    protected final int CACHE_BUFFER_SIZE = 16 * 1024;

    private void skipReadBuffer(int length) {
        int cache = inCacheSize - inCachePos;
        if (length > cache) {
            length -= cache;
            try {
                docReader.skip(length);
            } catch (Exception e) {
                e.printStackTrace();
            }
            fillCacheBuffer();
        } else {
            inCachePos += length;
        }
    }

    private int fillCacheBuffer() {
        inCachePos = 0;

        int size = 0;
        try {
            size = docReader.read(inCache, 0, inCacheSize);
            if (size == -1) {
                isEOF = true;
                inCacheSize = 0;
                return -1;
            }
        } catch (Exception e) {
            isEOF = true;
            return -1;
        }

        if (size != inCacheSize) {
            inCacheSize = size;
        }

        return size;
    }

    private int readCharInt() {
        int chi;
        chi = backreader.pop();
        if (chi >= 0) {
            return chi;
        }

        if (inCache == null) {
            inCacheSize = CACHE_BUFFER_SIZE;
            inCache = new char[inCacheSize];
            inCachePos = inCacheSize;
        } else if (inCacheSize == 0) {
            return -1;
        }

        if (inCacheSize == inCachePos) {
            int size = fillCacheBuffer();
            if (size < 0) {
                return -1;
            }
        }

        return (int) inCache[inCachePos++];
    }

    private char readChar() throws Exception {
        int chi = readCharInt();
        if (chi < 0) {
            throw new Exception("End of stream reached");
        }

        return (char) chi;
    }

    public Parser(Reader reader) {
        this.docReader = reader;
        this.isEOF = false;
        this.backreader = new Backreader();
    }

    /**
     * Checks whether document end was reached or not.<br>
     * This method must be used instead of comparing {@code next()} result to null
     *
     * @return true if document end has NOT been reached yet.
     * @see next()
     */
    public boolean hasNext() {
        return !isEOF;
    }

    private int skipSpaces() {
        int c = 0;
        for (;;) {
            int chi = readCharInt();
            if (chi < 0) {
                return chi;
            }

            char ch = (char) chi;

            if (ch != '\t' && ch != '\n' && ch != '\r' && ch != ' ') {
                backreader.push(ch);
                break;
            }
            c = 1;
        }
        return c;
    }

    private String readNodeName() {
        return readName();
    }

    private String readName() {
        int chi;
        StringBuilder name = new StringBuilder(STRING_BUFFER_SIZE);

        chi = readCharInt();
        if (chi < 0) {
            return null;
        }

        if ("qazxswedcvfrtgbnhyujmkiolpQWERTYUIOPASDFGHJKLZXCVBNM".indexOf((char) chi) == -1) {
            backreader.push((char) chi);
            return null;
        }

        name.append((char) chi);

        for (;;) {
            chi = readCharInt();
            if (chi < 0) {
                return null;
            }

            if ("qazxswedcvfrtgbnhyujmkiolpQWERTYUIOPASDFGHJKLZXCVBNM0147852369".indexOf((char) chi) == -1) {
                backreader.push((char) chi);
                break;
            }

            name.append((char) chi);
        }

        return name.toString();
    }

    private String readAttributeValue() {
        if (skipSpaces() < 0) {
            return null;
        }

        StringBuilder value = new StringBuilder(STRING_BUFFER_SIZE);
        int chi, quote = -1;

        chi = readCharInt();
        if (chi < 0) {
            return null;
        }

        if ((char) chi == '/' || (char) chi == '>') {
            backreader.push((char) chi);
            return null;
        }

        if ((char) chi == '\'' || (char) chi == '"') {
            quote = chi;
        } else {
            value.append((char) chi);
        }

        for (;;) {
            chi = readCharInt();
            if (chi < 0) {
                return null;
            }

            if (quote != -1 && (char) chi == quote) {
                break;
            } else if (quote != -1 && (char) chi == '\\') {
                chi = readCharInt();
                if (chi < 0) {
                    return null;
                }

                if (chi != quote) {
                    value.append('\\');
                }
            } else if (quote == -1) {
                if ("> \t\r\n".indexOf((char) chi) != -1) {
                    backreader.push((char) chi);
                    break;
                } else if ((char) chi == '/') {
                    chi = readCharInt();

                    if ((char) chi != '>') {
                        value.append('/');
                    } else {
                        backreader.push((char) chi);
                        break;
                    }
                }
            }

            value.append((char) chi);
        }

        return value.toString();
    }

    private AttributeSet readNodeAttributes() {
        SimpleAttributeSet attributes = new SimpleAttributeSet();

        for (;;) {
            Attribute attr = readNodeAttribute();
            if (attr == null) {
                break;
            }

            attributes.addAttribute(attr.getName(), attr.getValue());
        }

        return attributes;
    }

    private Attribute readNodeAttribute() {
        if (skipSpaces() < 0) {
            return null;
        }

        String name = readName();
        String value = null;

        if (name == null || skipSpaces() < 0) {
            return null;
        }

        int chi = readCharInt();
        if ((char) chi == '=') {
            value = readAttributeValue();
        }

        return new Attribute(name, value);
    }

    private Element readDeclCommentNode() {
        StringBuilder string = new StringBuilder(STRING_BUFFER_SIZE);

        boolean comment = false;
        if (0 == getPreview(2).compareTo("--")) {
            comment = true;
            skip(2);
        }

        for (;;) {
            int chi;
            chi = readCharInt();

            if (chi < 0) {
                return null;
            }

            if (comment) {
                if ((char) chi == '-') {
                    if (0 == getPreview(2).compareTo("->")) {
                        skip(2);
                        break;
                    }
                }
            } else {
                if ((char) chi == '>') {
                    break;
                }
            }

            string.append((char) chi);
        }

        if (comment) {
            return new CommentElement(string.toString());
        } else {
            return new DeclarationElement(string.toString());
        }
    }

    private Element readXMLNode() {
        String name = readNodeName();
        if (name == null) {
            return null;
        }

        AttributeSet attributes = readNodeAttributes();
        if (skipSpaces() < 0) {
            return null;
        }

        if (getPreview(2).compareTo("?>") != 0) {
            return null;
        }

        skip(2);
        return new XmlNodeElement(name, attributes);
    }

    private Element readNode() {
        int chi;
        String name;
        boolean closingNode = false;

        chi = readCharInt();
        if (chi < 0) {
            return null;
        }
        switch ((char) chi) {
            case '/':
                closingNode = true;
                break;
            case '?':
                return readXMLNode();
            case '!':
                return readDeclCommentNode();
            default:
                backreader.push((char) chi);
        }

        name = readNodeName();
        if (name == null) {
            try {
                char ch[] = new char[1];
                ch[0] = readChar();

                String b = new String(ch);
                return new TextElement(b);

            } catch (Exception e_) {
                return null;
            }
        }

        if (!closingNode) {
            AttributeSet attributes = readNodeAttributes();

            boolean singleNode = false;

            if (skipSpaces() < 0) {
                return null;
            }

            chi = readCharInt();
            if (chi < 0) {
                return null;
            }
            if ((char) chi == '/') {
                chi = readCharInt();
                if (chi < 0) {
                    return null;
                } else if ((char) chi == '>') {
                    singleNode = true;
                }
            }

            if (singleNode) {
                return new SingleNodeElement(name, attributes);
            } else {
                lastStartedNode = name;
                return new NodeBeginElement(name, attributes);
            }

        } else {
            if (skipSpaces() < 0) {
                return null;
            }

            chi = readCharInt();
            if (chi < 0) {
                return null;
            }
            return new NodeEndElement(name);
        }
    }

    private Element readOther() {
        StringBuilder other = new StringBuilder(STRING_BUFFER_SIZE);
        int j, lastValidChar = 0;

        for (j = 0;; ++j) {
            int chi = 0;

            chi = readCharInt();
            if (chi < 0) {
                break;
            }

            if ((char) chi == '<') {
                backreader.push((char) chi);
                break;
            }

            other.append((char) chi);
        }

        String result = other.toString();
        if (result.isEmpty()) {
            return null;
        }

        return new TextElement(result);
    }

    private void skip(int size) {
        size = backreader.skip(size);
        if (size > 0) {
            skipReadBuffer(size);
        }
    }

    private String getPreview(int size) {
        StringBuilder preview = new StringBuilder(size);
        int chi;
        for (; size > 0; size--) {
            chi = readCharInt();
            if (chi < 0) {
                backreader.pushString(preview.toString());
                break;
            }

            preview.append((char) chi);
        }

        String s = preview.toString();
        backreader.pushString(s);
        return s;
    }

    private Element readSpecialNodeContent(String nodeName) {
        StringBuilder content = new StringBuilder(STRING_BUFFER_SIZE);
        for (;;) {
            int chi;
            chi = readCharInt();
            if (chi < 0) {
                return null;
            }

            if ((char) chi == '<') {
                String pre = getPreview(nodeName.length() + 1);
                if (pre.equalsIgnoreCase("/" + nodeName)) {
                    backreader.push('<');
                    break;
                }
            }

            content.append((char) chi);
        }

        lastStartedNode = null;
        return new TextElement(content.toString());
    }

    /**
     * Reads the next element and return it, if exists.
     * <p><b>NOTE:</b> This method may return null anytime, but this does not
     * mean the end of document has been reached. A null result should be
     * ignored and this method should then be called again.</p>
     * <p>The way to check if document end has been reached is calling
     * {@code hasNext()}</p>
     *
     * @see hasNext()
     * @return next element in document
     */
    public final Element next() {
        int chi;

        if (lastStartedNode != null) {
            if (lastStartedNode.equalsIgnoreCase("script")) {
                return readSpecialNodeContent("script");
            } else if (lastStartedNode.equalsIgnoreCase("style")) {
                return readSpecialNodeContent("style");
            }

            lastStartedNode = null;
        }

        if (skipSpaces() < 0) {
            return null;
        }

        chi = readCharInt();
        if (chi < 0) {
            return null;
        }

        if ((char) chi == '<') {
            return readNode();
        } else {
            backreader.push((char) chi);
            return readOther();
        }
    }
}
